package cc.android.testapp.xp.hooker.adblock.core;

import android.app.Application;
import android.content.Context;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import cc.android.testapp.util.CLog;
import cc.android.testapp.util.CTool;
import cc.android.testapp.xp.XPTool;
import cc.android.testapp.xp.hooker.adblock.util.XHook;
import cc.commons.util.extra.CList;
import cc.commons.util.interfaces.IFilter;
import cc.commons.util.reflect.ClassUtil;
import cc.commons.util.reflect.MethodUtil;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;


public class BlockAdSDK {

    private static final List<ClassLoader> mHandledLoader = new ArrayList<>();

    public static void onHook(Application pAdd) {
        ClassLoader tLoader = pAdd.getClassLoader();
        if (mHandledLoader.contains(tLoader)) {
            return;
        }
        mHandledLoader.add(tLoader);

        AdSdkBlockConfig tCFG = new AdSdkBlockConfig("QQGDT", "com.qq.e.")  //QQGDT
                .addCons("ads.splash.SplashAD").addCons("ads.splash.TGSplashAD")
                .addCons("ads.contentad.ContentAD").addCons("ads.contentad.ContentAD")
                .addCons("ads.nativ.NativeAD").addCons("ads.interstitial.InterstitialAD")
                .addCons("ads.interstitial2.UnifiedInterstitialAD").addCons("ads.banner2.UnifiedBannerAD")
                .addCons("ads.banner.BannerView").addCons("ads.rewardvideo.RewardVideoAD")
                .addCons("ads.hybrid.HybridAD").addCons("a.m.AdConfig");
        if (ClassUtil.isClassLoaded("com.qq.e.comm.managers.GDTADManager", tLoader)) {
            tCFG.addEmptyMethod("comm.managers.GDTADManager", "initWith", true, null, null);
        } else {
            String tName = tCFG.mPackage + ".comm.managers.IGDTAdManager";
            if (ClassUtil.isClassLoaded(tName, tLoader)) {
                try {
                    Class<?> tIManager = tLoader.loadClass(tName);
                    ArrayList<Class<?>> tClazzs = XPTool.getPackgeClass(tLoader, "com.qq.e.comm.managers", false);
                    for (Class<?> sClazz : tClazzs) {
                        if (tIManager.isAssignableFrom(sClazz)) {
                            try {
                                for (Method sMethod : MethodUtil.getDeclaredMethod(sClazz, (method) -> {
                                    Class<?>[] tParams = method.getParameterTypes();
                                    return tParams.length > 1 && tParams[0].equals(Context.class) && tParams[1].equals(String.class);
                                })) {
                                    if (sMethod.getReturnType() == boolean.class) {
                                        tCFG.addEmptyMethod(sMethod, false, XC_MethodReplacement.returnConstant(false));
                                        break;
                                    }
                                }
                            } catch (IllegalStateException ignored) {
                            }
                        }
                    }
                } catch (Throwable e) {
                    CLog.log("检索QQADSDK的GDTADManager时出错", e);
                }
            }
        }
        String tSDKName = "comm.managers.GDTAdSdk";
        String tName = tCFG.mPackage + "." + tSDKName;
        if (ClassUtil.isClassLoaded(tName, tLoader)) {
            for (Method sMethod : MethodUtil.getMethodWithParam(ClassUtil.getClass(tName, tLoader), true, Context.class, String.class)) {
                tCFG.addEmptyMethod(sMethod, false, null);
            }
        }
        tCFG.init(tLoader);


        new AdSdkBlockConfig("ByteDance", "com.bytedance.sdk.openadsdk.") //穿山甲
                .addBuilder("TTAdConfig$Builder", false)
                .addBuilder("AdSlot$Builder", true).init(tLoader);

        new AdSdkBlockConfig("KuaiShou", "com.kwad.sdk.api") //快手
                .addBuilder("SdkConfig$Builder", false)
                .addEmptyMethod("KsAdSDK", "init").init(tLoader);

        new AdSdkBlockConfig("Baidu", "com.baidu.mobads.sdk.api") //百度
                .addBuilder("BDAdConfig$Builder", false).init(tLoader);

        new AdSdkBlockConfig("CQSDK", "com.cqyh.cqadsdk") //CQAD
                .addBuilder("CQAdSDKConfig$Builder", false).init(tLoader);

        new AdSdkBlockConfig("GroMore", "com.bytedance.msdk.api.v2") //GoMore
                .addBuilder("GMAdConfig$Builder", false).init(tLoader);

        new AdSdkBlockConfig("ADMore", "cn.admobiletop.adsuyi") //ADMore
                .addBuilder("config.ADSuyiInitConfig$Builder", false)
                .blockMethod("ad.ADSuyiSplashAd", "isReleased", new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        param.setResult(true);
                    }
                }).init(tLoader);

        new AdSdkBlockConfig("AdMobile", "admsdk.library.config") //Tianmu
                .addBuilder("AdmAdConfig", false).init(tLoader);

        new AdSdkBlockConfig("Tianmu", "com.tianmu.config") //Tianmu
                .addBuilder("TianmuInitConfig$Builder", false).init(tLoader);

        blockMSDKAD(tLoader);
        blockFaceBookAD(tLoader);
        blockUnity3DAD(tLoader);
        blockWindAD(tLoader);
        blockAdvlibAD(tLoader);
        blockAndroidAD(tLoader);
        blockIronSourceAD(tLoader);
    }

    private static void blockAndroidAD(ClassLoader pLoader) {
        if (!ClassUtil.isClassLoaded("com.google.android.gms.ads.MobileAds", pLoader)) return;

        HashMap<String, String> tAds = new HashMap<>();
        tAds.put("com.google.android.gms.ads.MobileAds", "initialize(");
        tAds.put("com.google.android.gms.ads.AdLoader", "loadAd(");
        tAds.put("com.google.android.gms.ads.BaseAdView", "loadAd(");
        tAds.put("com.google.android.gms.ads.interstitial.InterstitialAd", "loadAd(");
        tAds.put("com.google.android.gms.ads.appopen.AppOpenAd", "loadAd(");
        tAds.put("com.google.android.gms.ads.rewarded.RewardedAd", "loadAd(");
        tAds.put("com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd", "loadAd(");

        tAds.forEach((k, v) -> {
            if (ClassUtil.isClassLoaded(k, pLoader))
                XHook.with(pLoader).atClass(k).match(v).declared(true).hookBefore((pMParam, obj, objArr)
                        -> pMParam.setResult((Object) null));
        });

    }

    private static void blockMSDKAD(ClassLoader pLoader) {
        try {
            Class<?> loadClass = pLoader.loadClass("com.mbridge.msdk.system.a");
            XHook.with(pLoader).atClass(loadClass).match("init(").hookBefore((pMParam, obj, objArr)
                    -> pMParam.setResult((Object) null));
            XHook.with(pLoader).atClass(loadClass).match("initAsync(").hookBefore((pMParam, obj, objArr)
                    -> pMParam.setResult((Object) null));
            XposedBridge.hookAllConstructors(pLoader.loadClass(
                    "com.mbridge.msdk.out.MBRewardVideoHandler"), clearParamStr(false));
        } catch (Throwable ignored) {
        }
    }

    private static void blockUnity3DAD(ClassLoader pLoader) {
        try {
            XHook.with(pLoader)
                    .atClass(pLoader.loadClass("com.unity3d.ads.UnityAds"))
                    .match("initialize(").hookBefore((pMParam, obj, objArr)
                            -> pMParam.setResult((Object) null));
        } catch (Throwable ignored) {
        }
    }

    private static void blockWindAD(ClassLoader pLoader) {
        try {
            pLoader.loadClass("com.sigmob.windad.WindAdOptions");
            XposedHelpers.findAndHookConstructor("com.sigmob.windad.WindAdOptions"
                    , pLoader, String.class, String.class, new XC_MethodHook() {
                        protected void beforeHookedMethod(MethodHookParam pMParam) {
                            pMParam.args[0] = "";
                            pMParam.args[1] = "";
                        }
                    });
        } catch (Throwable ignored) {
        }
    }

    private static void blockAdvlibAD(ClassLoader pLoader) {
        try {
            XposedHelpers.findAndHookMethod("com.iclicash.advlib.ui.banner.ADBanner"
                    , pLoader, "UpdateView"
                    , pLoader.loadClass("com.iclicash.advlib.core.ICliBundle")
                    , new XC_MethodHook() {
                        protected void beforeHookedMethod(MethodHookParam pMParam) {
                            pMParam.setResult((Object) null);
                        }
                    });
        } catch (Throwable ignored) {
        }
    }

    private static XC_MethodHook clearParamStr(boolean pClearNumber) {
        return new XC_MethodHook() {
            protected void beforeHookedMethod(XC_MethodHook.MethodHookParam pParam) {
                if (pParam.args == null) return;

                for (int i = 0; i < pParam.args.length; i++) {
                    if (pParam.args[i] instanceof String) {
                        pParam.args[i] = "233";
                    } else if (pClearNumber && ((pParam.args[i] instanceof Integer)
                            || (pParam.args[i] instanceof Float)
                            || (pParam.args[i] instanceof Double))) {
                        pParam.args[i] = 233;
                    }
                }
            }
        };
    }

    private static void clearAdBuilder(Class<?> tClazz, boolean pClearNumber) {
        XC_MethodHook tHook = clearParamStr(pClearNumber);
        for (Method sM : tClazz.getDeclaredMethods()) {
            if (sM.getParameterTypes().length == 1
                    && sM.getParameterTypes()[0] == String.class
                    && (sM.getReturnType() == tClazz || sM.getReturnType() == void.class)) {
                XposedHelpers.findAndHookMethod(tClazz
                        , sM.getName(), String.class, tHook);
            }
        }
    }

    private static void blockFaceBookAD(ClassLoader pLoader) {
        try {
            pLoader.loadClass("com.facebook.ads.AudienceNetworkAds");
            XposedHelpers.findAndHookMethod("com.facebook.ads.AudienceNetworkAds", pLoader, "initialize", Context.class, new XC_MethodHook() {
                protected void beforeHookedMethod(MethodHookParam pMParam) {
                    pMParam.args[0] = null;
                }
            });
        } catch (Throwable ignored) {
        }
    }

    private static void blockIronSourceAD(ClassLoader pLoader) {
        try {
            Class<?> loadClass = pLoader.loadClass("com.ironsource.mediationsdk.IronSource");
            XHook.with(pLoader).atClass(loadClass).match("init(").hookBefore(new XHook.HookMethod() {
                @Override
                public void onHook(XC_MethodHook.MethodHookParam pMParam, Object obj, Object... pParams) {
                    pMParam.setResult((Object) null);
                }
            });
            XHook.with(pLoader).atClass(loadClass).match("initISDemandOnly(").hookBefore(new XHook.HookMethod() {
                @Override
                public void onHook(XC_MethodHook.MethodHookParam pMParam, Object obj, Object... pParams) {
                    pMParam.setResult((Object) null);
                }
            });
        } catch (Throwable ignored) {
        }
    }

    public static class AdSdkBlockConfig {
        public final String mName;
        /**
         * SDK包名,末尾不带点
         */
        public final String mPackage;

        public final HashSet<String> mConstructors = new HashSet<>();
        public final HashMap<String, Boolean> mBuilders = new HashMap<>();
        public final HashMap<AdMethod, XC_MethodHook> mEmptyMethods = new HashMap<>();

        public AdSdkBlockConfig(String pName, String pPackage) {
            this.mName = pName;
            if (pPackage.endsWith(".")) pPackage = pPackage.substring(0, pPackage.length() - 1);
            this.mPackage = pPackage;
        }

        /**
         * 添加需要清空构造函数参数的类
         *
         * @param pClazzs 类名
         * @return 自身
         */
        public AdSdkBlockConfig addCons(String... pClazzs) {
            for (String sStr : pClazzs) {
                if (sStr.startsWith(".")) sStr = sStr.substring(1);
                this.mConstructors.add(this.mPackage + "." + sStr);
            }

            return this;
        }

        /**
         * 添加一个需要清空配置的Builer类
         * <p>如果Builder的参数为String或者数字,将设置为其他数值</p>
         *
         * @param pBuilder 需要清空配置的Builder的名字,包含副包
         * @return 自己
         */
        public AdSdkBlockConfig addBuilder(String pBuilder, boolean pClearNumber) {
            if (pBuilder.startsWith(".")) pBuilder = pBuilder.substring(1);

            this.mBuilders.put(this.mPackage + "." + pBuilder, pClearNumber);
            return this;
        }

        public AdSdkBlockConfig blockMethod(String pClazz, String pMethod, @Nonnull XC_MethodHook pHook) {
            return addEmptyMethod(pClazz, pMethod, false, null, pHook);
        }

        public AdSdkBlockConfig blockMethod(String pClazz, String pMethod, @Nonnull XC_MethodHook pHook
                , @Nullable IFilter<Method> pFilter) {
            return addEmptyMethod(pClazz, pMethod, false, pFilter, pHook);
        }

        public AdSdkBlockConfig addEmptyMethod(Method pMethod, boolean pClearNumber, @Nullable XC_MethodHook pHandle) {
            this.mEmptyMethods.put(new AdMethod(pMethod), pHandle == null ? clearParamStr(pClearNumber) : pHandle);
            return this;
        }

        public AdSdkBlockConfig addEmptyMethod(String pClazz, String pMethod) {
            return addEmptyMethod(pClazz, pMethod, false, null, null);
        }

        public AdSdkBlockConfig addEmptyMethod(String pClazz, String pMethod, boolean pClearNumber) {
            return addEmptyMethod(pClazz, pMethod, pClearNumber, null, null);
        }

        public AdSdkBlockConfig addEmptyMethod(String pClazz, String pMethod, boolean pClearNumber
                , @Nullable IFilter<Method> pFilter, @Nullable XC_MethodHook pHandle) {
            if (pClazz.startsWith(".")) pClazz = pClazz.substring(1);

            AdMethod tAM = new AdMethod(this.mPackage + "." + pClazz, pMethod, pFilter);
            this.mEmptyMethods.put(tAM, pHandle == null ? clearParamStr(pClearNumber) : pHandle);
            return this;
        }

        public String dectorClass(ClassLoader pLoader, String... pClazzs) {
            for (String sStr : pClazzs) {
                if (sStr.startsWith(".")) sStr = sStr.substring(1);
                if (CTool.classExist(pLoader, this.mPackage + "." + sStr)) return sStr;
            }

            return pClazzs[0]; //不抛出异常
        }

        public boolean init(ClassLoader pLoader) {
            StringBuilder tResult = new StringBuilder();
            boolean tHasHook = false;

            if (!this.mConstructors.isEmpty()) tResult.append("Constructor:");
            boolean tFirst = true;
            for (String sClazz : this.mConstructors) {
                tFirst = false;
                try {
                    XposedBridge.hookAllConstructors(pLoader.loadClass(sClazz), clearParamStr(false));
                    tResult.append(1).append(',');
                    tHasHook = true;
                } catch (Throwable e) {
                    tResult.append(0).append(',');
                }
            }
            if (!tFirst) {
                tResult.deleteCharAt(tResult.length() - 1);
                tFirst = true;
            }

            if (!this.mBuilders.isEmpty()) tResult.append("|Builder:");
            for (Map.Entry<String, Boolean> sBuilder : this.mBuilders.entrySet()) {
                tFirst = false;
                try {
                    clearAdBuilder(pLoader.loadClass(sBuilder.getKey()), sBuilder.getValue());
                    tResult.append(1).append(',');
                    tHasHook = true;
                } catch (Throwable e) {
                    tResult.append(0).append(',');
                }
            }

            if (!tFirst) {
                tResult.deleteCharAt(tResult.length() - 1);
                tFirst = true;
            }

            if (!this.mEmptyMethods.isEmpty()) tResult.append("|MethodE:");
            for (Map.Entry<AdMethod, XC_MethodHook> sEM : this.mEmptyMethods.entrySet()) {
                tFirst = false;
                try {
                    List<Method> tMEs = sEM.getKey().findMethod(pLoader);
                    for (Method sM : tMEs) {
                        XposedBridge.hookMethod(sM, sEM.getValue());
                    }
                    tResult.append(1).append(',');
                    tHasHook = true;
                } catch (Throwable e) {
                    tResult.append(0).append(',');
                }
            }
            if (!tFirst) tResult.deleteCharAt(tResult.length() - 1);

            if (tHasHook) CLog.log("[ADBSDK] " + this.mName + " hooked: " + tResult);
            return true;
        }
    }

    static class AdMethod {
        Method mMethod;
        final String mClazz;
        final String mName;
        final IFilter<Method> mMFilter;

        AdMethod(Method pMethod) {
            mMethod = pMethod;
            this.mClazz = pMethod.getDeclaringClass().getName();
            this.mName = pMethod.getName();
            this.mMFilter = null;
        }

        AdMethod(String pClazz, String pMethod, @Nullable IFilter<Method> pMFilter) {
            this.mClazz = pClazz;
            this.mName = pMethod;
            this.mMFilter = pMFilter;
        }

        public List<Method> findMethod(ClassLoader pLoader) throws ClassNotFoundException {
            if (mMethod != null) return Collections.singletonList(mMethod);
            Class<?> tClazz = pLoader.loadClass(this.mClazz);
            CList<Method> tFindM = MethodUtil.getMethodIgnoreParam(tClazz, this.mName, false);
            if (this.mMFilter != null && !tFindM.isEmpty()) {
                Iterator<Method> tIt = tFindM.iterator();
                while (tIt.hasNext()) if (!this.mMFilter.accept(tIt.next())) tIt.remove();
            }
            return tFindM;
        }

        @Override
        public String toString() {
            return this.mClazz + "." + this.mName;
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }
    }

}
